/* 用户行为收集：
积累行为，批量发送
不保证可达（发送失败不重试）
不落磁盘，所以可能会丢失
保证有序发送，无法保证服务器顺序接收
*/
import moment from 'moment';

import { SQLError } from './wcdb';
import { RowData } from './db';
import Log from './log';
import Net from './netInfo';

export interface ActionData {
  [key: string]: number | string | boolean;
}

export interface ReportStorageProvider {
  append(content: string): Promise<number>;

  load(): Promise<RowData[]>;

  count(): Promise<number>;

  clearUtil(id: number): Promise<number>;
}

const TAG = 'ActionTracker';

class ActionTracker {
  /* actions的格式
    JSON.stringify({
      timestamp
      event
      ... data:ActionData
    });
  */
  private actionCount: number = 0;

  private handler: NodeJS.Timer | null = null;

  private lastSendTime: number = 0;

  private TIMEOUT: number = 10 * 1000;

  private MAXN: number = 10;

  private sending: boolean = false;

  private remoteReport: ((params: string) => Promise<any>) | null = null;

  private storageProvider: ReportStorageProvider | null = null;

  private ticket: number = 0;

  public start(
    action: (params: string) => Promise<any>,
    provider: ReportStorageProvider
  ): void {
    const that = this;
    this.remoteReport = action;
    this.storageProvider = provider;

    this.handler = setInterval((): void => {
      that.trySend();
    }, 1000);

    // 这里是异步操作，可能加载成功之前就已经有reportAction发出
    // 使用 += 会导致actionCount在race condition下比正常大一些，触发trySend后恢复正常
    this.storageProvider
      .count()
      .then((count: number): void => {
        Log.i(TAG, `load existed c=${count} ac=${that.actionCount}`);
        that.actionCount += count;
      })
      .catch((err: SQLError): void => {
        Log.e(TAG, `load existed count e=${err.message}`);
      });

    // 启动就触发一次发送事件, 尽早获得数据
    // this.lastSendTime = new Date().valueOf();
  }

  public stop(): void {
    if (this.handler) {
      clearInterval(this.handler);
      this.handler = null;
    }
  }

  public reportAction(
    name: string,
    data: ActionData = {},
    immediately: boolean = false
  ): void {
    if (!this.storageProvider) {
      return;
    }

    const content = JSON.stringify({
      timestamp: moment().format('YYYY-MM-DD HH:mm:ss.SSS'),
      event: name,
      ...data
    });
    this.actionCount += 1;

    if (immediately) {
      this.ticket += 1;
    } else if (this.actionCount >= this.MAXN) {
      this.ticket += 1;
    }

    const that = this;
    this.storageProvider.append(content).then((insertId: number): void => {
      Log.i(TAG, `report action id=${insertId} c=${content}`);

      if (immediately) {
        that.trySend();
      }
    });
  }

  private trySend(): void {
    if (
      Net.getNetType() === 'none' ||
      Net.getNetType() === 'unknown' ||
      Net.getNetType() === '2g'
    ) {
      return;
    }

    if (this.sending) {
      return;
    }
    const now = new Date().valueOf();
    if (now - this.lastSendTime > this.TIMEOUT) {
      this.sendAll();
      return;
    }

    if (this.ticket > 0) {
      this.sendAll();
    }
  }

  private sendAll(): void {
    if (!this.storageProvider) {
      return;
    }

    if (!this.remoteReport) return;

    if (this.ticket > 0) {
      this.ticket -= 1;
    }

    if (this.actionCount === 0) {
      return;
    }

    this.sending = true;

    const that = this;
    let maxId = 0;
    this.storageProvider
      .load()
      .then((datas: RowData[]): Promise<any> | undefined => {
        const actions = [];
        for (let i = 0; i < (datas && datas.length); i += 1) {
          actions.push(datas[i].content);
          maxId = Number(datas[i].id);
        }

        if (that.remoteReport) {
          return that.remoteReport(JSON.stringify(actions));
        }
        return undefined;
      })
      .then((): void => {
        that.clear(maxId);
      })
      .catch((): void => {
        that.sending = false;
      });
  }

  private clear(maxId: number): void {
    if (!this.storageProvider) {
      return;
    }

    const that = this;
    this.storageProvider
      .clearUtil(maxId)
      .then((numAffected: number): void => {
        that.actionCount -= numAffected;
        that.ticket = 0; // 这可能导致race condition下ticket偏少, 不影响正常逻辑
        that.lastSendTime = new Date().valueOf();
        that.sending = false;
      })
      .catch((): void => {
        that.lastSendTime = new Date().valueOf();
        that.sending = false;
      });
  }
}

const tracker = new ActionTracker();
export default tracker;
